﻿using System;
using System.Data;
using System.Collections;
using System.Collections.Generic;

namespace BizElements.Core
{
    /// <summary>A method that creates new entity and initializes members for which values are defined in the array.</summary>
    /// <param name="table">Metadata for table/view to which the entity belongs to.</param>
    /// <param name="entityState">Indicates the state of entity with regard to data-source.</param>
    /// <param name="values">Array which contains values for all properties mapped to database columns.</param>
    public delegate TEntity EntityInitializer<TEntity>(IDbTable table, EntityState entityState, object[] values)
        where TEntity : IEntity;

    /// <summary>Builds entity objects from retrieved data using provided <b>CreateEntity</b> method.</summary>
    public class EntityReader<TEntity> : IObjectReader
        where TEntity : IEntity, new()
    {
        #region Constructors.

        /// <summary>Initializes a new instance of <b>EntityReader</b> class.</summary>
        public EntityReader()
        {
        }

        /// <summary>Initializes a new instance of <b>EntityReader</b> class.</summary>
        /// <param name="createEntityMethod">Method delegate that creates entities. 
        /// If <b>null</b> is provided then default implementation that uses reflection and <see cref="IRecordTransformer.FromObjectArray"/> method will be used.</param>
        public EntityReader(EntityInitializer<TEntity> createEntityMethod)
        {
            this.CreateEntity = createEntityMethod;
        }

        #endregion

        /// <summary>Sets method that creates entities.</summary>
        /// <value>Delegate. Default is <b>null</b> which means that default implementation that uses reflection and <see cref="IRecordTransformer.FromObjectArray"/> method will be used.</value>
        public EntityInitializer<TEntity> CreateEntity { get; set; }

        /// <summary>Builds objects from retrieved data.</summary>        
        /// <param name="openedDataReader">Opened data reader that contains all fields required to build objects.</param>
        /// <param name="collection">A collection to fill with objects built from data.</param>
        public void FillCollection(IDataReader openedDataReader, IList collection)
        {
            // Many generic collections implement IList.Add(object item) method by casting for each added item, ie. internaly use: "Add((T)item);"
            // Slightly better performance may be achieved when using specific execution path that avoids casting by using ICollection<T>.Add(T item) directly.
            ICollection<TEntity> genericCollection = collection as ICollection<TEntity>;
            if (genericCollection != null)
                FillGenericCollection(openedDataReader, genericCollection);
            else
                FillNonGenericList(openedDataReader, collection);
        }

        #region Fill generic collections

        /// <summary>Builds entites from retrieved data using <see cref="IRecordTransformer.FromObjectArray(object[])"/> method.</summary>
        /// <param name="openedDataReader">Opened data reader that contains all fields required to initialize entities.</param>
        /// <param name="collection">A collection to fill with entities built from data.</param>
        private void FillGenericCollection(IDataReader openedDataReader, ICollection<TEntity> collection)
        {
            if (openedDataReader.IsClosed)
                throw new ArgumentException("Reader is closed", "openedDataReader");

            if (this.CreateEntity == null)
                this.CreateEntity = NewEntityFromObjectArray;

            string[] columnNamesSortedByInitializationOrder = GetColumnNamesSortedForUsageInFromObjectArray();
            int[] columnIndexMap = MapReaderToEntityFieldIndexes(openedDataReader, columnNamesSortedByInitializationOrder);
            IDbTable table = new TEntity().Table;
            if (columnIndexMap == null)
                FillFromAlignedReader(collection, openedDataReader, table);
            else
                FillFromPartialOrMisalignedReader(collection, openedDataReader, table, columnIndexMap);
        }

        private void FillFromAlignedReader(ICollection<TEntity> collection, IDataReader openedDataReader, IDbTable table)
        {
            // Fastest scenario: data reader fields match entity field completely.
            // It's safe to reuse same array because GetValues() always overwrites all members. Memory is allocated only once.
            object[] values = new object[openedDataReader.FieldCount];
            while (openedDataReader.Read())
            {
                openedDataReader.GetValues(values);
                TEntity entity = CreateEntity(table, EntityState.Synchronized, values);
                collection.Add(entity);
            }
        }

        private void FillFromPartialOrMisalignedReader(ICollection<TEntity> collection, IDataReader openedDataReader, IDbTable table, int[] propertyToColumnIndexMap)
        {
            object[] fetchedValues = new object[propertyToColumnIndexMap.Length];
            object[] sortedPropertyValues = new object[propertyToColumnIndexMap.Length];
            while (openedDataReader.Read())
            {
                openedDataReader.GetValues(fetchedValues);
                ReorderValues(fetchedValues, sortedPropertyValues, propertyToColumnIndexMap);
                TEntity entity = CreateEntity(table, EntityState.Synchronized, sortedPropertyValues);
                collection.Add(entity);
            }
        }

        private static void ReorderValues(object[] fetchedValues, object[] sortedPropertyValues, int[] propertyToColumnIndexMap)
        {
            for (int i = 0; i < sortedPropertyValues.Length; i++)
            {
                int idxInFetchedValues = propertyToColumnIndexMap[i];
                sortedPropertyValues[i] = (idxInFetchedValues >= 0) ? fetchedValues[idxInFetchedValues] : null;
            }
        }

        #endregion

        #region Fill non-generic lists.

        private void FillNonGenericList(IDataReader openedDataReader, IList collection)
        {
            if (openedDataReader.IsClosed)
                throw new ArgumentException(Messages.EntityFiller_ReaderIsClosed, "openedDataReader");

            if (this.CreateEntity == null)
                this.CreateEntity = NewEntityFromObjectArray;

            string[] columnNamesSortedByInitializationOrder = GetColumnNamesSortedForUsageInFromObjectArray();
            int[] columnIndexMap = MapReaderToEntityFieldIndexes(openedDataReader, columnNamesSortedByInitializationOrder);
            IDbTable table = new TEntity().Table;
            if (columnIndexMap == null)
                FillNonGenericListFromAlignedReader(collection, openedDataReader, table);
            else
                FillNonGenericListFromPartialOrMisalignedReader(collection, openedDataReader, table, columnIndexMap);
        }

        private void FillNonGenericListFromAlignedReader(IList collection, IDataReader openedDataReader, IDbTable table)
        {
            // Fastest scenario: data reader fields match entity field completely.
            // It's safe to reuse same array because GetValues() always overwrites all members. Memory is allocated only once.
            object[] values = new object[openedDataReader.FieldCount];
            while (openedDataReader.Read())
            {
                openedDataReader.GetValues(values);
                TEntity entity = CreateEntity(table, EntityState.Synchronized, values);
                collection.Add(entity);
            }
        }

        private void FillNonGenericListFromPartialOrMisalignedReader(IList collection, IDataReader openedDataReader, IDbTable table, int[] propertyToColumnIndexMap)
        {
            object[] fetchedValues = new object[propertyToColumnIndexMap.Length];
            object[] sortedPropertyValues = new object[propertyToColumnIndexMap.Length];
            while (openedDataReader.Read())
            {
                openedDataReader.GetValues(fetchedValues);
                ReorderValues(fetchedValues, sortedPropertyValues, propertyToColumnIndexMap);
                TEntity entity = CreateEntity(table, EntityState.Synchronized, sortedPropertyValues);
                collection.Add(entity);
            }
        }

        #endregion

        #region GetColumnNamesSortedForUsageInFromObjectArray.

        /// <summary>Gets table column names sorted in the same order as they are initialized in entity's <see cref="IRecordTransformer.FromObjectArray(object[])"/> method.</summary>
        /// <returns>Array of columns names ordered in same way as <see cref="IDbTable.Columns"/> collection.</returns>
        /// <remarks><para>Default implementation assumes that the order of field initialization in <b>FromObjectArray</b> method
        /// equals the order of items in <see cref="IDbTable.Columns"/> collection. In case that this not true then this
        /// method must be overrided and it has to return column names ordered in same way as object array in the <b>FromObjectArray</b>
        /// are accessed.</para>
        /// <para>Templates shipped with BizElements generator generate code that read object array in the same order as definied
        /// in <see cref="IDbTable.Columns"/> collection.</para></remarks>
        protected string[] GetColumnNamesSortedForUsageInFromObjectArray()
        {
            IDbTable tbl = new TEntity().Table;
            string[] colNames = new string[tbl.Columns.Count];
            for (int i = 0; i < colNames.Length; i++)
                colNames[i] = tbl.Columns[i].ColumnName;

            return colNames;
        }

        #endregion

        #region MapReaderToEntityFieldIndexes.

        private static int[] MapReaderToEntityFieldIndexes(IDataReader dataReader, string[] columnNamesSortedByInitializationOrder)
        {
            // Map reader to entity field indexes.
            // Missing columns are maped as -1.
            // If reader columns are perfectly matched to entity field initialization order then null is returned.
            if (dataReader.FieldCount == columnNamesSortedByInitializationOrder.Length)
                return CreateFullPropertyToColumnIndexMap(dataReader, columnNamesSortedByInitializationOrder);
            else
                return CreatePartialPropertyToColumnIndexMap(dataReader, columnNamesSortedByInitializationOrder);
        }

        private static int[] CreateFullPropertyToColumnIndexMap(IDataReader dataReader, string[] columnNamesSortedByInitializationOrder)
        {
            // All entity fields are fetched but reader columns may be misaligned.
            // If reader columns are perfectly matched to entity field initialization order then null is returned.
            bool isMapRequired = false;
            int[] map = new int[columnNamesSortedByInitializationOrder.Length];
            int idxInFromObjectArray = 0;
            foreach (string fieldName in columnNamesSortedByInitializationOrder)
            {
                int idxInReader = dataReader.GetOrdinal(fieldName);
                map[idxInFromObjectArray] = idxInReader;
                if (idxInFromObjectArray != idxInReader)
                    isMapRequired = true;

                idxInFromObjectArray++;
            }

            if (isMapRequired)
                return map;
            else
                return null;
        }

        private static int[] CreatePartialPropertyToColumnIndexMap(IDataReader dataReader, string[] columnNamesSortedByInitializationOrder)
        {
            // Reader fetches less data than it's required to create fully initialized entities.
            // Missing columns are maped as -1.
            Dictionary<string, int> readerFields = GetDataReaderFieldIndexes(dataReader);
            int[] map = new int[columnNamesSortedByInitializationOrder.Length];
            for (int i = 0; i < columnNamesSortedByInitializationOrder.Length; i++)
            {
                string fieldName = columnNamesSortedByInitializationOrder[i].ToUpperInvariant();
                int idxInReader;
                bool isFetched = readerFields.TryGetValue(fieldName, out idxInReader);
                if (!isFetched)
                    idxInReader = -1;

                map[i] = idxInReader;
            }

            return map;
        }

        private static Dictionary<string, int> GetDataReaderFieldIndexes(IDataReader dataReader)
        {
            Dictionary<string, int> readerFields = new Dictionary<string, int>(dataReader.FieldCount);
            for (int idxInReader = 0; idxInReader < dataReader.FieldCount; idxInReader++)
            {
                string fieldName = dataReader.GetName(idxInReader).ToUpperInvariant();
                readerFields[fieldName] = idxInReader;
            }
            return readerFields;
        }

        #endregion

        #region NewEntityFromObjectArray - default implementation that uses reflection and IEntity.FromObjectArray().

        private static TEntity NewEntityFromObjectArray(IDbTable table, EntityState entityState, object[] fieldValues)
        {
            // Default implementation. Slower than generated NewEntity and NewPartialEntity methods but a lot faster than going through DataTable.
            for (int i = 0; i < fieldValues.Length; i++)
            {
                if (fieldValues[i] == DBNull.Value)
                    fieldValues[i] = null;
            }

            TEntity entity = (TEntity)Activator.CreateInstance(typeof(TEntity));
            entity.FromObjectArray(fieldValues);
            entity.EntityState = EntityState.Synchronized;
            return entity;
        }

        #endregion
    }
}